using System;
using UnityEngine;

namespace FPLibrary
{
	public class FPMathf
	{
		public static Fix64 Sin(Fix64 f) =>
			FPMath.Sin(f);

		// Returns the cosine of angle /f/ in radians.
		public static Fix64 Cos(Fix64 f) =>
			FPMath.Cos(f);

		// Returns the tangent of angle /f/ in radians.
		public static Fix64 Tan(Fix64 f) =>
			FPMath.Tan(f);

		// Returns the arc-sine of /f/ - the angle in radians whose sine is /f/.
		public static Fix64 Asin(Fix64 f) =>
			FPMath.Asin(f);

		// Returns the arc-cosine of /f/ - the angle in radians whose cosine is /f/.
		public static Fix64 Acos(Fix64 f) =>
			FPMath.Acos(f);

		// Returns the arc-tangent of /f/ - the angle in radians whose tangent is /f/.
		public static Fix64 Atan(Fix64 f) =>
			FPMath.Atan(f);

		// Returns the angle in radians whose ::ref::Tan is @@y/x@@.
		public static Fix64 Atan2(Fix64 y, Fix64 x) =>
			FPMath.Atan2(y, x);

		// Returns square root of /f/.
		public static Fix64 Sqrt(Fix64 f) =>
			FPMath.Sqrt(f);

		// Returns the absolute value of /f/.
		public static Fix64 Abs(Fix64 f) =>
			FPMath.Abs(f);

		// Returns the absolute value of /value/.
		public static int Abs(int value) =>
			Math.Abs(value);

		/// *listonly*
		public static Fix64 Min(Fix64 a, Fix64 b) =>
			a < b ? a : b;

		// Returns the smallest of two or more values.
		public static Fix64 Min(params Fix64[] values)
		{
			if (values.Length == 0)
				return Fix64.Zero;

			var min = values[0];
			foreach (var value in values)
				if (value < min)
					min = value;

			return min;
		}

		/// *listonly*
		public static int Min(int a, int b) =>
			a < b ? a : b;

		// Returns the smallest of two or more values.
		public static int Min(params int[] values)
		{
			if (values.Length == 0)
				return 0;

			var min = values[0];
			foreach (var value in values)
				if (value < min)
					min = value;

			return min;
		}

		/// *listonly*
		public static Fix64 Max(Fix64 a, Fix64 b) =>
			a > b ? a : b;

		// Returns largest of two or more values.
		public static Fix64 Max(params Fix64[] values)
		{
			if (values.Length == 0)
				return Fix64.Zero;

			var max = values[0];
			foreach (var value in values)
				if (value > max)
					max = value;

			return max;
		}

		/// *listonly*
		public static int Max(int a, int b) =>
			a > b ? a : b;

		// Returns the largest of two or more values.
		public static int Max(params int[] values)
		{
			if (values.Length == 0)
				return 0;

			var max = values[0];
			foreach (var value in values)
				if (value > max)
					max = value;

			return max;
		}

		// // Returns /f/ raised to power /p/.
		// public static Fix64 Pow(Fix64 f, Fix64 p) => Fix64.
		// 	FPMath.Pow(f, p);
		//
		// // Returns e raised to the specified power.
		// public static Fix64 Exp(Fix64 power) =>
		// 	FPMath.Exp(power);
		//
		// // Returns the logarithm of a specified number in a specified base.
		// public static Fix64 Log(Fix64 f, Fix64 p) =>
		// 	FPMath.Log(f, p);
		//
		// // Returns the natural (base e) logarithm of a specified number.
		// public static Fix64 Log(Fix64 f) =>
		// 	FPMath.Log(f);
		//
		// // Returns the base 10 logarithm of a specified number.
		// public static Fix64 Log10(Fix64 f) =>
		// 	FPMath.Log10(f);

		// Returns the smallest integer greater to or equal to /f/.
		public static Fix64 Ceil(Fix64 f) =>
			FPMath.Ceiling(f);

		// Returns the largest integer smaller to or equal to /f/.
		public static Fix64 Floor(Fix64 f) =>
			FPMath.Floor(f);

		// Returns /f/ rounded to the nearest integer.
		public static Fix64 Round(Fix64 f) =>
			FPMath.Round(f);

		// Returns the smallest integer greater to or equal to /f/.
		public static int CeilToInt(Fix64 f) =>
			(int)FPMath.Ceiling(f);

		// Returns the largest integer smaller to or equal to /f/.
		public static int FloorToInt(Fix64 f) =>
			(int)FPMath.Floor(f);

		// Returns /f/ rounded to the nearest integer.
		public static int RoundToInt(Fix64 f) =>
			(int)FPMath.Round(f);

		// Returns the sign of /f/.
		public static Fix64 Sign(Fix64 f) =>
			f >= Fix64.Zero ? Fix64.One : -Fix64.One;

		// Clamps a value between a minimum float and maximum float value.
		public static Fix64 Clamp(Fix64 value, Fix64 min, Fix64 max)
		{
			if (value < min)
				value = min;
			else if (value > max)
				value = max;
			return value;
		}

		// Clamps value between min and max and returns value.
		// Set the position of the transform to be that of the time
		// but never less than 1 or more than 3
		//
		public static int Clamp(int value, int min, int max)
		{
			if (value < min)
				value = min;
			else if (value > max)
				value = max;
			return value;
		}

		// Clamps value between 0 and 1 and returns value
		public static Fix64 Clamp01(Fix64 value)
		{
			if (value < Fix64.Zero)
				return Fix64.Zero;

			if (value > Fix64.One)
				return Fix64.One;

			return value;
		}

		// Interpolates between /a/ and /b/ by /t/. /t/ is clamped between 0 and 1.
		public static Fix64 Lerp(Fix64 a, Fix64 b, Fix64 t) =>
			a + (b - a) * Clamp01(t);

		// Interpolates between /a/ and /b/ by /t/ without clamping the interpolant.
		public static Fix64 LerpUnclamped(Fix64 a, Fix64 b, Fix64 t) =>
			a + (b - a) * t;

		// Same as ::ref::Lerp but makes sure the values interpolate correctly when they wrap around 360 degrees.
		public static Fix64 LerpAngle(Fix64 a, Fix64 b, Fix64 t)
		{
			var delta = Repeat((b - a), 360);
			if (delta > 180)
				delta -= 360;
			return a + delta * Clamp01(t);
		}

		// Moves a value /current/ towards /target/.
		public static FPVector2 MoveTowards(FPVector2 current, FPVector2 target, Fix64 maxDelta)
		{
			var xPosition = MoveTowards(current.x, target.x, maxDelta);
			var yPosition = MoveTowards(current.y, target.y, maxDelta);
			return new FPVector2(xPosition, yPosition);
		}

		// Moves a value /current/ towards /target/.
		public static Fix64 MoveTowards(Fix64 current, Fix64 target, Fix64 maxDelta)
		{
			if (Abs(target - current) <= maxDelta)
				return target;
			return current + Sign(target - current) * maxDelta;
		}

		// Same as ::ref::MoveTowards but makes sure the values interpolate correctly when they wrap around 360 degrees.
		public static Fix64 MoveTowardsAngle(Fix64 current, Fix64 target, Fix64 maxDelta)
		{
			var deltaAngle = DeltaAngle(current, target);
			if (-maxDelta < deltaAngle && deltaAngle < maxDelta)
				return target;
			target = current + deltaAngle;
			return MoveTowards(current, target, maxDelta);
		}

		// Interpolates between /min/ and /max/ with smoothing at the limits.
		public static Fix64 SmoothStep(Fix64 from, Fix64 to, Fix64 t)
		{
			t = Clamp01(t);
			t = -2.0F * t * t * t + 3.0F * t * t;
			return to * t + from * (1F - t);
		}

		// //*undocumented
		// public static Fix64 Gamma(Fix64 value, Fix64 absMax, Fix64 gamma)
		// {
		// 	var negative = value < Fix64.Zero;
		// 	var absVal   = Abs(value);
		// 	if (absVal > absMax)
		// 		return negative ? -absVal : absVal;
		//
		// 	var result = Pow(absVal / absMax, gamma) * absMax;
		// 	return negative ? -result : result;
		// }

		// Compares two floating point values if they are similar.
		public static bool Approximately(Fix64 a, Fix64 b) =>

			// If a or b is zero, compare that the other is less or equal to epsilon.
			// If neither a or b are 0, then find an epsilon that is good for
			// comparing numbers at the maximum magnitude of a and b.
			// Floating points have about 7 significant digits, so
			// 1.000001f can be represented while 1.0000001f is rounded to zero,
			// thus we could use an epsilon of 0.000001f for comparing values close to 1.
			// We multiply this epsilon by the biggest magnitude of a and b.
			Abs(b - a) < Max(0.000001f * Max(Abs(a), Abs(b)), Fix64.Epsilon * 8);

		public static Fix64 SmoothDamp(Fix64 current, Fix64 target, ref Fix64 currentVelocity, Fix64 smoothTime, Fix64 maxSpeed)
		{
			var deltaTime = Time.deltaTime;
			return SmoothDamp(current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime);
		}

		public static Fix64 SmoothDamp(Fix64 current, Fix64 target, ref Fix64 currentVelocity, Fix64 smoothTime)
		{
			var deltaTime = Time.deltaTime;
			var maxSpeed  = Fix64.PositiveInfinity;
			return SmoothDamp(current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime);
		}

		// Gradually changes a value towards a desired goal over time.
		public static Fix64 SmoothDamp(Fix64 current, Fix64 target, ref Fix64 currentVelocity, Fix64 smoothTime, Fix64 maxSpeed, Fix64 deltaTime)
		{
			// Based on Game Programming Gems 4 Chapter 1.10
			smoothTime = Max(0.0001F, smoothTime);
			var omega = 2F / smoothTime;

			var x          = omega     * deltaTime;
			var exp        = Fix64.One / (Fix64.One + x + 0.48F * x * x + 0.235F * x * x * x);
			var change     = current - target;
			var originalTo = target;

			// Clamp maximum speed
			var maxChange = maxSpeed * smoothTime;
			change = Clamp(change, -maxChange, maxChange);
			target = current - change;

			var temp = (currentVelocity + omega * change) * deltaTime;
			currentVelocity = (currentVelocity - omega           * temp) * exp;
			var output = target                + (change + temp) * exp;

			// Prevent overshooting
			if (originalTo - current > Fix64.Zero == output > originalTo)
			{
				output          = originalTo;
				currentVelocity = (output - originalTo) / deltaTime;
			}

			return output;
		}

		public static Fix64 SmoothDampAngle(Fix64 current, Fix64 target, ref Fix64 currentVelocity, Fix64 smoothTime, Fix64 maxSpeed)
		{
			var deltaTime = Time.deltaTime;
			return SmoothDampAngle(current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime);
		}

		public static Fix64 SmoothDampAngle(Fix64 current, Fix64 target, ref Fix64 currentVelocity, Fix64 smoothTime)
		{
			var deltaTime = Time.deltaTime;
			var maxSpeed  = Fix64.PositiveInfinity;
			return SmoothDampAngle(current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime);
		}

		// Gradually changes an angle given in degrees towards a desired goal angle over time.
		public static Fix64 SmoothDampAngle(Fix64 current, Fix64 target, ref Fix64 currentVelocity, Fix64 smoothTime, Fix64 maxSpeed, Fix64 deltaTime)
		{
			target = current + DeltaAngle(current, target);
			return SmoothDamp(current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime);
		}

		// Loops the value t, so that it is never larger than length and never smaller than 0.
		public static Fix64 Repeat(Fix64 t, Fix64 length) =>
			Clamp(t - Floor(t / length) * length, Fix64.Zero, length);

		// PingPongs the value t, so that it is never larger than length and never smaller than 0.
		public static Fix64 PingPong(Fix64 t, Fix64 length)
		{
			t = Repeat(t, length * 2F);
			return length - Abs(t - length);
		}

		// Calculates the ::ref::Lerp parameter between of two values.
		public static Fix64 InverseLerp(Fix64 a, Fix64 b, Fix64 value)
		{
			if (a != b)
				return Clamp01((value - a) / (b - a));

			return Fix64.Zero;
		}

		// Calculates the shortest difference between two given angles.
		public static Fix64 DeltaAngle(Fix64 current, Fix64 target)
		{
			var delta = Repeat((target - current), 360.0F);
			if (delta > 180.0F)
				delta -= 360.0F;
			return delta;
		}

		public static int NextPowerOfTwo(int value)
		{
			value -= 1;
			value |= value >> 16;
			value |= value >> 8;
			value |= value >> 4;
			value |= value >> 2;
			value |= value >> 1;
			return value + 1;
		}

		public static int ClosestPowerOfTwo(int value)
		{
			var nextPower = NextPowerOfTwo(value);
			var prevPower = nextPower >> 1;
			if (value - prevPower < nextPower - value)
				return prevPower;

			return nextPower;
		}

		public static bool IsPowerOfTwo(int value) =>
			(value & (value - 1)) == 0;
	}
}
